```latex
\newcommand{\commonfolder}{../../common-files}

\input{\commonfolder/header}
\input{\commonfolder/copyright}

\newcommand{\miniVPN}{{\tt MiniVPN}\xspace}
\newcommand{\vpnFigs}{./Figs}

\lhead{\bfseries SEED Labs -- VPN Lab: The Container Version}

\newcommand{\hostu}{{\tt U}\xspace}
\newcommand{\hostv}{{\tt V}\xspace}

\begin{document}

\begin{center}
{\LARGE VPN Lab: The Container Version}
\end{center}

\seedlabcopyright{2020}

% *******************************************
% SECTION
% *******************************************
\section{概述}

虚拟专用网络（VPN）是在公共网络（通常为互联网）之上构建的私有网络。在VPN内部的计算机可以像实际物理隔离于外界的私人网络那样安全地通信，尽管他们的流量可能会经过公共网络。VPN使员工能够在出差时安全访问公司内网；它还允许公司将私有网络扩展到全国乃至世界各地。

本实验的目标是帮助学生理解如何工作。我们将专注于一种特定类型的VPN（最常见的类型），这种类型基于传输层构建。我们将从零开始构建一个非常简单的VPN，并使用该过程来说明每部分技术是如何工作的。真正的VPN程序有两个基本组成部分：封装和加密。本实验仅关注封装部分，帮助学生理解封装技术，因此本实验中的隧道不进行加密。我们还有一项更全面的实验室涵盖了加密部分。

以下是本课程涵盖的主题：

\begin{itemize}[noitemsep]
    \item 虚拟专用网络
    \item TUN/TAP虚拟接口 
    \item IP隧道化 
    \item 路由
\end{itemize}

\paragraph{阅读资料和视频。}有关TUN/TAP虚拟接口及其工作原理的详细信息可以在以下内容中找到：

\begin{itemize}
    \item 《SEED书籍》第19章，\seedbook
    \item 《SEED讲座》第8节，\seedisvideo
\end{itemize}

\paragraph{相关实验室。}本实验仅涵盖VPN的封装部分，而完整的VPN还需要保护其隧道。我们有一个单独的实验室，称为“VPN实验室”，这是一个全面的实验室，涵盖了封装和保护两部分内容。学生可以先完成这个封装实验室。在学习了公钥基础设施（PKI）和传输层安全协议（TLS）之后，他们可以继续进行综合性的VPN实验室。

\paragraph{实验环境。} \seedenvironmentC

\newpage
% *******************************************
% SECTION
% ******************************************* 
\section{任务1：网络设置}

我们将创建一个在客户端计算机与网关之间建立的VPN隧道，使客户端计算机可以通过网关安全地访问私有网络。我们至少需要三台机器：客户端（作为主机 \hostu），服务器端（路由器/网关）和私有网络中的主机（Host \hostv）。网络设置如图~\ref{vpn:fig:labsetup}所示。

\begin{figure}[htb]
    \centering
    \includegraphics[width=0.9\textwidth]{./Figs/VPN_2lans.pdf}
    \caption{实验环境设置}
    \label{vpn:fig:labsetup}
\end{figure}

在实际应用中，客户端计算机和服务器端通过互联网连接。为了简化起见，在本实验室中我们将这两台机器直接连接到同一个局域网（LAN），即这个局域网模拟了互联网。

第三台机器 Host \hostv 是私有网络中的一个主机。用户想要通过VPN隧道与位于私有网络之外的 Host \hostu 通信。为了模拟此设置，我们连接了Host \hostv 到服务器端（同时也是一个网关）。在这种情况下，Host \hostv 不可以直接从互联网访问；同样地也不可以直接从 Host \hostu 访问。

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\paragraph{实验设置。} 
\input{\commonfolder/container/setup}

\paragraph{共享文件夹。} 在本实验中，我们需要编写自己的代码并在容器内部运行。
\input{\commonfolder/container/volumes}

\paragraph{包嗅探。}
\input{\commonfolder/container/sniffing}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\paragraph{测试。} 请进行以下测试以确保实验室环境设置正确：

\begin{itemize}[noitemsep]
    \item Host \hostu 可以与 VPN 服务器通信。
    \item VPN 服务器可以与 Host \hostv 通信。
    \item Host \hostu 应该无法与 Host \hostv 进行通信。 
    \item 在路由器上运行 \texttt{tcpdump}，并在每个网络上嗅探流量。显示您可以捕获数据包。
\end{itemize}

% *******************************************
% SECTION
% ******************************************* 
\section{任务2：创建并配置 TUN 接口}

我们将要构建的 VPN 隧道基于TUN/TAP 技术。TUN 和 TAP 是虚拟网络内核驱动程序；它们实现了完全由软件支持的网络设备。

TAP（类似于网络TAP）模拟以太网设备，操作于如以太网帧这样的二层包；TUN（类似于网络隧道）模拟网络层设备，并处理如IP包这样的三层包。通过 TUN/TAP，我们可以创建虚拟网络接口。

用户空间程序通常会附加到 TUN/TAP 虚拟网络接口。操作系统通过 TUN/TAP 网络接口发送的包将被传递给用户空间程序。另一方面，程序通过 TUN/TAP 接口发送的包会被注入操作系统的网络堆栈。对于操作系统而言，这些包似乎是从虚拟网络接口外部来的。

当一个程序附加到 TUN/TAP 接口时，内核向该接口发出的 IP 包将被引导至程序。另一方面，由程序写入该接口的 IP 包将会被注入内核，好像来自外界通过这个虚拟网络接口。程序可以使用标准 {\tt read()} 和 {\tt write()} 系统调用来从或发送给虚拟接口接收和发送包。

本任务的目标是熟悉 TUN/TAP 技术。我们将进行几个实验来学习该技术的细节。我们将在以下 Python 程序的基础上进行这些实验，并在本实验室中修改此基础代码。这部分代码已包含在 zip 文件中的 \texttt{volumes} 文件夹中。

\begin{lstlisting}[caption={创建 TUN 接口 (\texttt{tun.py})}, label=vpn:list:create_tun]
#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# 创建 TUN 接口
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'tun%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

# 获取接口名称
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("接口名称: {}".format(ifname))

while True:
   time.sleep(10)
\end{lstlisting}

% -------------------------------------------
% 子部分
% ------------------------------------------- 
\subsection{任务2.a：接口名称} 

我们将使用 Host \hostu 运行 \texttt{tun.py} 程序。使上面的 \texttt{tun.py} 可执行，并以 root 权限运行它。请参见以下命令：

\begin{lstlisting}
// 使 Python 程序可执行 
# chmod a+x tun.py

// 使用 root 权限运行程序
# tun.py
\end{lstlisting}

一旦程序执行，它会阻塞。您可以在另一个终端中进入容器内的新 shell。然后打印出机器上的所有接口。请在运行以下命令后报告您的观察：

\begin{lstlisting}
# ip address
\end{lstlisting}

你应该能够找到一个名为 \texttt{tun0} 的接口。本任务中的工作是更改 \texttt{tun.py} 程序，使其使用的接口名称前缀不再是 \texttt{tun} ，而是使用您的姓氏作为前缀。例如，如果您姓 smith，您应该使用 \texttt{smith} 作为前缀。如果您的姓氏较长，请使用前五个字符。请展示您的结果。

% -------------------------------------------
% 子部分
% ------------------------------------------- 
\subsection{任务2.b：设置 TUN 接口}

此时，TUN 接口还不可用，因为还没有对其进行配置。我们需要做两件事才能使用该接口。首先，我们需要为它分配一个 IP 地址。其次，我们需要将该接口启动起来，因为它仍然处于关闭状态。我们可以使用以下两个命令来进行配置：

\begin{lstlisting}
// 为接口分配 IP 地址
# ip addr add 192.168.53.99/24 dev tun0

// 启动接口
# ip link set dev tun0 up
\end{lstlisting}

为了使生活更简单，学生可以向 \texttt{tun.py} 添加以下两行代码，从而由程序自动执行配置。

\begin{lstlisting}
os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname)) 
os.system("ip link set dev {} up".format(ifname))              
\end{lstlisting}

运行上述两个命令之后，请再次运行 \texttt{"ip address"} 命令，并报告您的观察。与配置命令执行前相比，它们有何不同？

% -------------------------------------------
% 子部分
% ------------------------------------------- 
\subsection{任务2.c：从 TUN 接口读取数据}

在这个任务中，我们将从 TUN 接口中读取数据。从中出来的任何东西都是 IP 包。我们可以将接收到的数据转换为 Scapy \texttt{IP} 对象，以便打印出每个字段的详细信息。请使用以下 \texttt{while} 循环替换 \texttt{tun.py} 中的循环：

\begin{lstlisting}
while True:
   # 从 TUN 接口获取一个包
   packet = os.read(tun, 2048)
   if packet:
      ip = IP(packet)
      print(ip.summary())
\end{lstlisting}

请在 Host \hostu 上运行修订后的 \texttt{tun.py} 程序，相应地配置 TUN 接口，然后进行以下实验。描述您的观察：

\begin{itemize}
    \item 在 Host \hostu 上通过 \texttt{ping} 局域网中的一个主机。由 \texttt{tun.py} 程序打印出什么内容？发生了什么事情？为什么？

    \item 从 Host \hostu 发送 \texttt{ping} 到内部网络中的另一个局域网（\texttt{192.168.60.0/24}）中的一个主机。\texttt{tun.py} 是否打印出任何内容？为什么？
\end{itemize}

% -------------------------------------------
% 子部分
% ------------------------------------------- 
\subsection{任务2.d：向 TUN 接口写入数据} 

在这个任务中，我们将向 TUN 接口写入数据。由于这是一个虚拟网络接口，应用程序写入的数据将作为 IP 包出现在内核中。

我们将修改 \texttt{tun.py} 程序，在从 TUN 接口获取一个包之后，根据接收到的包构建一个新的包。然后我们将新包写回到 TUN 接口中。如何构造新包完全取决于学生。以下代码显示了如何将 IP 包写入到 TUN 接口中。

\begin{lstlisting}
# 通过 TUN 接口发送一个伪造包
newip  = IP(src='1.2.3.4', dst=ip.src)
newpkt = newip/ip.payload
os.write(tun, bytes(newpkt))
\end{lstlisting}

请根据以下要求修改 \texttt{tun.py} 的代码：

\begin{itemize}
    \item 如果从 TUN 接口接收到的包是一个 ICMP 检查请求包，则构造相应的检查回复包并写入到 TUN 接口中。提供证据证明代码按预期工作。
    
    \item 代替向接口写入 IP 包，向接口写入一些任意数据，并报告您的观察。
\end{itemize}

% *******************************************
% SECTION
% ******************************************* 
\section{任务3：通过隧道将 IP 包发送到 VPN 服务器} 

在这个任务中，我们将把从 TUN 接口接收到的 IP 包放入一个新的 UDP 载荷字段，并发送给另一台计算机。也就是说，我们将在一个新包内嵌入原包。这称为 IP 隧道化。隧道实现就是标准的客户端/服务器编程。可以在 TCP 或 UDP 之上建立。在这个任务中我们将使用 UDP。

\paragraph{服务器程序 \texttt{tun_server.py}。} 
我们将在 VPN 服务器上运行 \texttt{tun_server.py} 程序。该程序就是一个标准的 UDP 服务器程序，监听端口 \texttt{9090} 并打印出接收到的内容。程序假设 UDP 载荷字段中的数据是一个 IP 包，因此将载荷转换为 Scapy 的 \texttt{IP} 对象，并打印出封装包内的源和目的 IP 地址。

\begin{lstlisting}[caption={\texttt{tun_server.py}}, label=vpn:list:tun_server]
#!/usr/bin/env python3

from scapy.all import *

IP_A = "0.0.0.0"
PORT = 9090

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((IP_A, PORT))

while True:
   data, (ip, port) = sock.recvfrom(2048)
   print("{}:{} --> {}:{}".format(ip, port, IP_A, PORT))
   pkt = IP(data)
   print("   内部: {} --> {}".format(pkt.src, pkt.dst))
\end{lstlisting}

\paragraph{实现客户端程序 \texttt{tun_client.py}。} 
首先，我们需要修改 TUN 程序 \texttt{tun.py} 。让我们将其重命名为 \texttt{tun_client.py} 。使用 UDP 发送数据到另一台计算机可以使用标准的套接字编程完成。

用以下内容替换程序中的 \texttt{while} 循环：\texttt{SERVER\_IP} 和 \texttt{SERVER\_PORT} 应该被替换为在 VPN 服务器上运行的实际 IP 地址和端口号。

\begin{lstlisting}
# 创建 UDP 套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while True:
   # 从 TUN 接口获取一个包
   packet = os.read(tun, 2048)
   if packet:
      # 通过隧道发送包
      sock.sendto(packet, (SERVER_IP, SERVER_PORT))
\end{lstlisting}

\paragraph{测试。} 
在 VPN 服务器上运行 \texttt{tun_server.py} 程序，然后在 Host \hostu 上运行 \texttt{tun_client.py} 。要测试隧道是否工作，请 \texttt{ping} 属于 \texttt{192.168.53.0/24} 网络的任何 IP 地址。 服务器端打印出什么内容？为什么？

我们的最终目标是使用隧道访问私有网络中的主机（\texttt{192.168.60.0/24}）。让我们 \texttt{ping} Host \hostv，看看 ICMP 包是否通过隧道发送到 VPN 服务器。如果没有，请指出问题所在。你需要解决此问题，以便可以通过隧道发送 \texttt{ping} 包。这通过路由实现，即发往 \texttt{192.168.60.0/24} 网络的包应该被路由到 TUN 接口并交给 \texttt{tun_client.py} 程序处理。以下命令显示如何将路由表中添加一个条目：

\begin{lstlisting}
# ip route add <网络> dev <接口> via <路由器 IP>
\end{lstlisting}

请提供证据，以证明当您向 \texttt{192.168.60.0/24} 网络中的一个 IP 地址 \texttt{ping} 时，ICMP 包是通过隧道到达 \texttt{tun_server.py} 的。

% *******************************************
% SECTION
% ******************************************* 
\section{任务4：设置 VPN 服务器}

在 \texttt{tun_server.py} 获取隧道中的包后，它需要将包传递给内核，以便内核可以将其路由到最终目的地。这需要通过 TUN 接口完成，就像我们所做的事情一样。请修改 \texttt{tun_server.py} ，使其能够做到以下几点：

\begin{itemize}[noitemsep]
    \item 创建一个 TUN 接口并进行配置。
    
    \item 从套接字接口获取数据；将收到的数据视为 IP 包。
    
    \item 将包写入到 TUN 接口中。
\end{itemize}

在运行修改后的 \texttt{tun_server.py} 之前，我们需要启用IP转发。除非特别配置，计算机只会作为一个主机而不是网关工作。VPN服务器需要在其与私有网络之间的隧道之间转发包，因此它需要作为网关工作。我们需要为计算机开启 IP 转发功能，使其像一个网关一样运行。路由器容器已经启用了 IP 转发功能。您可以在 \texttt{docker-compose.yml} 中看到以下条目：

\begin{lstlisting}
sysctls:
        - net.ipv4.ip_forward=1
\end{lstlisting}

\paragraph{测试。} 如果一切设置正确，我们可以通过 Host \hostu 向 Host \hostv 发送 \texttt{ping} 消息。最终的 ICMP 回应包应该通过隧道到达 Host \hostv。
请展示您的证据。注意尽管 Host \hostv 会对 ICMP 包做出响应，但回包不会回到 Host \hostu ，因为我们还没有完成其他设置。因此，在此任务中仅需要展示（使用 Wireshark 或 tcpdump）ICMP 包已经到达了 Host \hostv 即可。

% *******************************************
% SECTION
% ******************************************* 
\section{任务5：处理双向流量}

到达这一点时，隧道的一个方向就完成了，即我们可以通过隧道从 Host \hostu 向 Host \hostv 发送包。如果我们查看 Host \hostv 上的 Wireshark 记录，可以看到 Host \hostv 已发送了回应，但包在某个地方被丢弃了。这是因为我们的隧道是单向的；我们需要设置其另一方向，以便返回流量能够通过隧道传回给 Host U。

要实现这一点，我们的 TUN 客户端和服务器程序需要从两个接口读取数据：TUN 接口和套接字接口。所有这些接口都表示为文件描述符，因此我们需要注意是否从它们中有数据到来。一种方法是不断轮询它们，并查看每个接口上是否有数据。这种方案的性能不佳，因为当没有数据时进程必须在空闲循环中持续运行。另一种方法是从一个接口读取数据。默认情况下，读操作阻塞，即如果没有任何数据，该进程将被暂停。当有可用数据时，该进程将继续执行。这样就不会浪费CPU时间。

基于读的操作会阻塞在一个接口上工作得很好。如果有程序等待多个接口的数据，则它不能在其中任何一个接口上阻塞。它必须一起同时阻塞所有这些接口。Linux 有一个叫做 \texttt{select()} 的系统调用，允许一个程序同时监控多个文件描述符。为了使用 \texttt{select()} ，我们需将要监视的所有文件描述符存储在一个集合中，然后将这个集合传递给 \texttt{select()} 系统调用，在有任意一个文件描述符上有可用数据时它会阻塞进程。我们可以检查哪个文件描述符接收到数据。以下Python代码片段使用 \texttt{select()} 监控 TUN 接口和套接字接口上的数据：

\begin{lstlisting}
# 假设已经创建了 sock 和 tun 文件描述符

while True:
  # 这将阻塞直到至少有一个接口准备好
  ready, _, _ = select.select([sock, tun], [], [])

  for fd in ready:
    if fd is sock:
       data, (ip, port) = sock.recvfrom(2048)
       pkt = IP(data)
       print("从套接字<==: {} --> {}".format(pkt.src, pkt.dst))
       ...（学生成为添加的代码）...

    if fd is tun:
       packet = os.read(tun, 2048)
       pkt = IP(packet)
       print("从 tun ==>: {} --> {}".format(pkt.src, pkt.dst))
       ...（学生成为添加的代码）...
\end{lstlisting}

学生可以用上述代码替换 \texttt{tun.py} 中的 \texttt{while} 循环。该代码不完整，学生需要完成它。

\paragraph{测试。} 
一旦完成了这些操作，我们就应该能够从 Host \hostu 与 Machine \hostv 进行通信，并且未加密的隧道现在已完成。
请使用 \texttt{ping} 和 \texttt{telnet} 命令展示 Wireshark 证据。在你的证据中指出数据包是如何流动的。

% *******************************************
% SECTION
% ******************************************* 
\section{任务6：隧道中断实验}

在 Host \hostu 上通过 \texttt{telnet} 连接到 Host \hostv，保持这个 \texttt{telnet} 会话打开。然后停止 \texttt{tun_client.py} 或 \texttt{tun_server.py} 程序。你看到你在键入的内容了吗？会发生什么事情呢？TCP连接会被中断吗？

现在让我们重新连接隧道（不要等待太久）。我们将再次运行 \texttt{tun_client.py} 和 \texttt{tun_server.py} 程序，并设置它们的 TUN 接口和路由（这里是您会发现将配置命令包含在程序中会使您的生活更轻松的地方）。一旦隧道重新建立，\texttt{telnet} 连接会发生什么？请描述并解释你的观察。

% *******************************************
% SECTION
% ******************************************* 
\section{任务7：Host \hostv 的路由实验}

在一个真正的VPN系统中，流量将会被加密（本课程没有涵盖这部分）。这意味着返回的流量必须从相同的隧道返回。如何将 Host \hostv 的返回流量路由到 VPN 服务器并不直观。我们的设置简化了这个问题。在我们的情况下，Host V 的路由表有一个默认设置：发往任何目的地址的数据包，除了 \texttt{192.168.60.0/24} 网络外，将自动被路由到VPN 服务器。

在现实世界中，Host \hostv 可能会与 VPN 服务器相隔几个跳跃点，且默认路由条目无法保证返回包会被路由回到 VPN 服务器。私有网络内的路由表必须正确设置以确保发往隧道另一端的数据包将被路由到 VPN 服务器。为了模拟此场景，我们将从 Host \hostv 删除默认条目，并向路由表中添加一个更具体的条目，以便可以将返回数据包路由回到 VPN 服务器。学生可以使用以下命令删除默认条目并添加新条目：

\begin{lstlisting}
// 删除默认条目
# ip route del default

// 添加一条新的路由条目
# ip route add <网络前缀> via <路由器 IP>
\end{lstlisting}

% *******************************************
% SECTION
% *******************************************
\section{任务8：私有网络之间的VPN}

\begin{figure}[htb]
    \centering
    \includegraphics[width=0.8\textwidth]{Figs/VPN_3lans.pdf}
    \caption{两个私有网络间的 VPN}
    \label{vpn:fig:two-private-network}
\end{figure}

在这个任务中，我们将在一个私有网络和另一个私有网络之间建立一个隧道。设置如图~\ref{vpn:fig:two-private-network}所示。

整个设置描述在 \texttt{docker-compose2.yml} 文件中，并且您可以使用 \texttt{"-f docker-compose2.yml"} 选项告诉 \texttt{docker-compose} 使用这个文件而不是默认的 \texttt{docker-compose.yml} 文件。 

\begin{lstlisting}
$ docker-compose -f docker-compose2.yml build
$ docker-compose -f docker-compose2.yml up
$ docker-compose -f docker-compose2.yml down
\end{lstlisting}

此设置模拟了一个组织有两个站点的情形，每个站点都有一个私有网络。唯一的连接这两个网络的方式是通过互联网。

您的任务是在这些两个站点之间建立一个隧道，使得这两个网络之间的通信将经过一个VPN隧道。您可以使用之前开发的代码，但您需要思考如何设置正确的路由，以便这两个私有网络间的包能够被路由到VPN隧道中。在你的报告中，请描述并解释你所做的事情。你需要提供证据来证明两个私有网络之间的数据包确实在通过一个VPN隧道。

% *******************************************
% SECTION
% ******************************************* 
\section{任务9：TAP 接口实验}

在这个任务中，我们将进行一个简单的 TAP 接口实验，以便学生可以了解这种接口的一些概念。与 TUN 接口工作的主要区别在于，TUN 接口的内核端连接到IP层，而TAP接口的内核端连接到MAC层。因此，经过TAP接口的数据包包括MAC头部，而经过TUN接口的数据包仅包括IP头部。

我们将使用以下程序进行实验，并只使用客户端容器（任一实验室环境设置都可）。创建 TUN 接口和 TAP 接口的代码非常相似；唯一的区别在于接口类型。对于 TAP 接口我们使用 \texttt{IFF\_TAP}，而对于 TUN 我们使用 \texttt{IFF\_TUN}。其余的代码相同，因此不包含在以下内容中。配置 TAP 接口的方式与配置 TUN 接口完全一样。

\begin{lstlisting}
...

tap = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'tap%d', (*@\textbf{IFF\_TAP}@*) | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tap, TUNSETIFF, ifr)
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
... 

while True:
   packet = os.read(tap, 2048)
   if packet:
      ether = Ether(packet)
      print(ether.summary())
\end{lstlisting}

上述代码简单地从 TAP 接口中读取数据。然后将数据转换为 Scapy 的 \texttt{Ether} 对象，并打印出所有字段的内容。试着 \texttt{ping} 属于 \texttt{192.168.53.0/24} 网络中的一个 IP 地址；报告并解释你的观察。

为了使这更有趣，当你从 TAP 接口接收到以太网帧时，你可以检查是否是一个 ARP 请求。如果是，请生成相应的 ARP 回复并将它写入到 TAP 接口中。以下提供了一个样本代码：

\begin{lstlisting}
while True:
   packet = os.read(tap, 2048)
   if packet:
      print("--------------------------------")
      ether = Ether(packet)
      print(ether.summary())

      # 发送一个伪造的 ARP 响应
      FAKE_MAC   = "aa:bb:cc:dd:ee:ff"
      if ARP in ether and ether[ARP].op == 1 :
         arp       = ether[ARP]
         newether  = Ether(dst=ether.src, src=FAKE_MAC)
         newarp    = ARP(psrc=arp.pdst, hwsrc=FAKE_MAC,
                         pdst=arp.psrc, hwdst=ether.src, op=2)
         newpkt     = newether/newarp

         print("***** 假的响应: {}".format(newpkt.summary()))
         os.write(tap, bytes(newpkt))
\end{lstlisting}

为了测试你的 TAP 程序，可以在任意 IP 地址上运行 \texttt{arping} 命令。此命令将通过指定的接口向指定的 IP 地址发送 ARP 请求。
如果你的伪造 ARP 回应 TAP 程序正常工作，则你应该能够接收到响应。以下是一些示例：

\begin{lstlisting}
arping -I tap0 192.168.53.33
arping -I tap0 1.2.3.4
\end{lstlisting}

% *******************************************
% SECTION
% ******************************************* 
\section{提交}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\input{\commonfolder/submission}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\end{document}
```